package com.hxy.es.service;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.apache.commons.lang3.StringUtils;
import org.elasticsearch.action.get.GetRequest;
import org.elasticsearch.action.get.GetResponse;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.text.Text;
import org.elasticsearch.index.query.*;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.aggregations.Aggregation;
import org.elasticsearch.search.aggregations.AggregationBuilder;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.Aggregations;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.lang.reflect.Field;
import java.util.*;

/**
 * @description: Es查询工具  es每次查询最多只能查出一万条
 * @program: springbootTest
 * @author: HCY
 * @create: 2022/04/19 15:53
 */
@Component
public class EsSelectService {


    @Autowired
    private  RestHighLevelClient esClient;

    // private final static RestHighLevelClient esClient = new RestHighLevelClient(RestClient.builder(new HttpHost("127.0.0.1", 9200,"http")));

    private int page = 0;
    private int pageSize = 10;

    /**实体中映射文档id的字段名称 **/
    private String idField = "id";

    /**查询总数量**/
    private Long total;

    /**索引名称**/
    private String indexName;

    /**高亮设置类**/
    private HighlightBuilder highlightBuilder;

    /**排序 可以多个字段排序 多次调用排序方法即可**/
    private Map<String,SortOrder> sortMap = new LinkedHashMap<>();


    /**
     * 设置分页开始参数数
     * @param page
     * @return
     */
    public EsSelectService page(int page){
        this.page = page;
        return this;
    }

    /**
     * 设置分页数目
     * @param pageSize
     * @return
     */
    public EsSelectService pageSize(int pageSize){
        this.pageSize = pageSize;
        return this;
    }

    /**
     * 设置操作的索引，必须要赋值
     * @param indexName
     * @return
     */
    public EsSelectService indexName(String indexName){
        if (StringUtils.isEmpty(indexName)) {
            throw new IllegalArgumentException(" requires indexName to be non-null");
        }
        this.indexName = indexName;
        return this;
    }

    /**
     * 设置实体中映射文档id的字段名称 默认实体字段名称就是id
     * @param idField
     * @return
     */
    public EsSelectService idField(String idField){
        this.idField = idField;
        return this;
    }

    /**
     * 获取查询的总数量
     * @return
     */
    public Long getTotal(){
        return this.total == null? 0 : this.total;
    }

    /**
     * 添加排序参数 可以多次调用，不同字段排序
     * @param field 排序字段
     * @param sortOrder SortOrder.ASC or SortOrder.DESC
     * @return
     */
    public EsSelectService sort(String field,SortOrder sortOrder){
        sortMap.put(field,sortOrder);
        return this;
    }

    /**
     * 设置高亮参数
     * @param files
     */
    public EsSelectService setHighlight(String preTags,String postTags, String...files){
        //设置高亮
        HighlightBuilder highlighter = new HighlightBuilder();
        for (String file : files) {
            highlighter.field(file);
        }
        //设置前后缀标签
        highlighter.preTags(preTags);
        highlighter.postTags(postTags);
        this.highlightBuilder = highlighter;
        return this;

    }

    /**
     * 将实体中的字段替换为高亮后的字段数据
     * @param searchHit
     * @param obj
     * @throws IllegalAccessException
     */
    private void replaceHitParam(SearchHit searchHit,Object obj) throws IllegalAccessException {
        // 获取高亮结果，替换goods中的title
        Map<String, HighlightField> highlightFields = searchHit.getHighlightFields();
        for (Map.Entry<String,HighlightField> entry: highlightFields.entrySet()) {
            String hiField = entry.getKey();
            //获取到高亮后带标签的值
            Text[] hitValue = entry.getValue().fragments();
            //反射替换实体对应字段的值为带高亮标签的值
            Field[] declaredFields = obj.getClass().getDeclaredFields();
            for (Field field : declaredFields) {
                field.setAccessible(true);
                if (field.getName().equals(hiField)) {
                    field.set(obj,hitValue[0].toString());
                }
            }
        }
    }


    /**
     * 构建ES查询方法
     * @param queryBuilder 查询条件
     * @param tClass  泛型class
     * @param <T> 泛型
     * @return
     * @throws Exception
     */
    public <T> List<T> esbuildSelect(QueryBuilder queryBuilder,Class<T> tClass) throws Exception {
        if (StringUtils.isEmpty(this.indexName)) {
            throw new RuntimeException("indexName不可为空，请先赋值！");
        }
        // 1.创建查询条件构建器SearchSourceBuilder
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        // 2.添加分页信息  不设置 默认10条
        sourceBuilder.from(this.page);
        sourceBuilder.size(this.pageSize);
        // 3.设置高亮参数
        if (null != this.highlightBuilder) {
            sourceBuilder.highlighter(this.highlightBuilder);
        }
        // 4.排序
        for (Map.Entry<String,SortOrder> entry: this.sortMap.entrySet()) {
            sourceBuilder.sort(entry.getKey(), entry.getValue());
        }
        // 5.指定查询条件
        sourceBuilder.query(queryBuilder);
        // 6.构建查询请求对象，指定查询的索引名称 添加查询条件构建器 SearchSourceBuilder
        SearchRequest searchRequest = new SearchRequest(this.indexName);
        searchRequest.source(sourceBuilder);
        // 7. 查询,获取查询结果
        SearchResponse searchResponse = esClient.search(searchRequest, RequestOptions.DEFAULT);
        // 8. 获取命中对象 SearchHits
        SearchHits hits = searchResponse.getHits();
        // 8.1 获取总记录数
        Long total = hits.getTotalHits().value;
        this.total = total;
        // 8.2 获取Hits数据 数组(字符为json格式)
        SearchHit[] dataHits = hits.getHits();
        //9.返回结果
        List<T> dataList = new ArrayList<>();
        for (SearchHit searchHit : dataHits) {
            // 获取数据切转为实体类
            String sourceAsString = searchHit.getSourceAsString();
            JSONObject jsonObj = JSONObject.parseObject(sourceAsString);
            String id = searchHit.getId();
            jsonObj.put(this.idField,id);
            T obj = JSON.parseObject(jsonObj.toJSONString(),tClass);
            //替换高亮字段
            if (null != this.highlightBuilder) {
                replaceHitParam(searchHit,obj);
            }
            dataList.add(obj);
        }
        return dataList;
    }


    /**
     * 根据id 查询数据
     * @param id
     * @param tClass
     * @param <T>
     * @return
     * @throws Exception
     */
    public <T> T selectDocById(Class<T> tClass,String id) throws Exception {
        //设置查询的索引、文档
        GetRequest indexRequest = new GetRequest(this.indexName,id);
        GetResponse response = esClient.get(indexRequest, RequestOptions.DEFAULT);
        String sourceAsString = response.getSourceAsString();
        if (StringUtils.isEmpty(sourceAsString)) {
            return null;
        }
        JSONObject resultObj = JSONObject.parseObject(sourceAsString);
        String idValue = response.getId();
        resultObj.put(this.idField,idValue);
        return JSON.parseObject(resultObj.toJSONString(),tClass);
    }

    /**
     * 查询所有数据 默认显示10条
     * @param tClass
     * @param <T>
     * @return
     * @throws Exception
     */
    public <T> List<T> matchAll(Class<T> tClass) throws Exception{
        QueryBuilder queryBuilder= QueryBuilders.matchAllQuery();
        return this.esbuildSelect(queryBuilder,tClass);
    }
    /**
     * match查询参数会进行分词查询，分词后的词以or的形式去查询
     * @param tClass
     * @param field
     * @param param
     * @param <T>
     * @return
     * @throws Exception
     */
    public <T> List<T> matchQuery(Class<T> tClass,String field,String param) throws Exception{
        QueryBuilder queryBuilder = QueryBuilders.matchQuery(field,param);
        return this.esbuildSelect(queryBuilder,tClass);
    }

    /**
     * multiMatch查询会对查询内容分词 字段可以接受通配符 *field
     *  用于一个关键词多个字段匹配查询，并且他们之间的关系是或者 列: param = field1 or param = field2
     *
     *  (注：multiMatch查询也可以加operator这个参数，or 和 and ,意思也是一样，但是mutil 字段与字段之间还是or的关系，operator只关系到分词后
     *      * 单个字段去查询时，分词之间是or还是and的关系去查询)
     * @param tClass
     * @param param
     * @param fields
     * @param <T>
     * @return
     * @throws Exception
     */
    public <T> List<T> multiMatchQuery(Class<T> tClass,String param,String...fields) throws Exception{
        QueryBuilder queryBuilder = QueryBuilders.multiMatchQuery(param,fields);
        return this.esbuildSelect(queryBuilder,tClass);
    }

    /**
     * match查询的细化，若果参数为or，就和match查询一个效果：查询参数分词后，然后去查询关系是or。
     * 如果参数为AND则参数分词后，查询的字段需要同时满足分词后的词，就是且的关系
     *
     * @param tClass 返回实体class
     * @param field  字段
     * @param param  查询参数
     * @param operator 类行枚举
     * @param <T>
     * @return
     * @throws Exception
     */
    public <T> List<T> matchBoolQuery(Class<T> tClass,String field,String param,Operator operator) throws Exception{
        QueryBuilder queryBuilder = QueryBuilders.matchQuery(field,param).operator(operator);
        return this.esbuildSelect(queryBuilder,tClass);
    }

    /**
     * 短语句查询  match_phrase首先会把 查询的 内容分词，同时文档还要满足以下两个条件才会被搜索到：
     *              1：分词后所有词项都要出现在该字段中（相当于 and 操作）
     *              2：搜索词项的顺序与字段结果中的词项顺序要一致。
     *              3、这个顺序一致时可以通过slop来设置，可以间隔多少
     *                  match_phrase与slop一起用，能保证分词间的邻近关系，
     *                  slop参数告诉match_phrase查询词条能够相隔多远时仍然将文档视为匹配，
     *                  默认是0。为0时 必须相邻才能被检索出来。
     *
     * 列子： 例如下面的语句，会把“白雪公主吃苹果”也能检索出来
     * {
     *      “query”: {
     *      “match_phrase”: {
     *      “content” : {
     *          “query” : “白雪公主和苹果”，
     *          “slop” : 1
     *      }
     * }
     * @param tClass 返回实体class
     * @param field  字段
     * @param param  查询参数
     * @param slop   相邻间隔多少 默认0
     * @param <T>
     * @return
     * @throws Exception
     */
    public <T> List<T> matchPhraseQuery(Class<T> tClass,String field,String param,int slop) throws Exception{
        if (slop == 0) { slop = 0; }
        QueryBuilder queryBuilder = QueryBuilders.matchPhraseQuery(field,param).slop(slop);
        return this.esbuildSelect(queryBuilder,tClass);
    }

    /**
     * match_phrase_prefix与match_phrase相同，除了它容许文本中最后一个词的前缀匹配。
     * 例如：若是用户输入 johnnie walker bl ，咱们但愿在它们完成输入搜索条件前就能获得：
     *      Johnnie Walker Black Label 和 Johnnie Walker Blue Label 。
     * @param tClass 返回实体class
     * @param field  字段
     * @param param  查询参数
     * @param slop   相邻间隔多少 默认0
     * @param <T>
     * @return
     * @throws Exception
     */
    public <T> List<T> matchPhrasePrefixQuery(Class<T> tClass,String field,String param,int slop) throws Exception{
        if (slop == 0) { slop = 0; }
        QueryBuilder queryBuilder = QueryBuilders.matchPhrasePrefixQuery(field,param).slop(slop);
        return this.esbuildSelect(queryBuilder,tClass);
    }

    /**
     * 会分词  查询字段可识别特殊符合 比如 且或 AND、OR、NOT、exists、_missing_等关键字(大写),不写符号分词后默认or去查询
     *      AND：表示条件与；
     *      OR：表示条件或；
     *      NOT：表示条件非；
     *      _exists_：表示存在，且是存在对应字段；
     *      _missing_：表示不存在或为空，此处为空建议用_exists_取非；
     *         2个字段之间是或者的关系去查询的满足其一即可
     * 用的列子：
     * 查询title索引字段中包含“北京大学”的文档，并且包含“浙江大学”的文档；
     *     title:北京大学 AND title:浙江大学 即  param= 北京大学 AND  浙江大学
     * 查询title索引字段中包含“北京大学”或者“浙江大学”，且type为“1”的文档；
     * (title:北京大学 OR title:浙江大学) AND type:1  param= 北京大学 OR  浙江大学
     * @param tClass
     * @param param
     * @param fields
     * @param <T>
     * @return
     * @throws Exception
     */
    public <T> List<T> queryStringQuery(Class<T> tClass,String param,String... fields) throws Exception{
        QueryStringQueryBuilder queryStringQueryBuilder = QueryBuilders.queryStringQuery(param);
        for (String field : fields) {
            queryStringQueryBuilder.field(field);
        }
        return this.esbuildSelect(queryStringQueryBuilder,tClass);
    }

    /**
     * term查询参数不分词查询（常用于字段keyword 、numeric、date精确查询），
     * 一个查询参数可以匹配多个字段。相当于：field = value1 or field = value2
     * @param tClass 返回实体
     * @param field  字段
     * @param params 查询参数组
     * @param <T>
     * @return
     * @throws Exception
     */
    public <T> List<T> termsQuery(Class<T> tClass,String field,String...params) throws Exception{
        QueryBuilder queryBuilder = QueryBuilders.termsQuery(field,params);
        return this.esbuildSelect(queryBuilder,tClass);
    }



    /**
     * wildcard 模糊查询 例如： "*黄*"  包含华字的
     *                        "黄*"   华字后边多个字符
     *                        "黄?"  华字后边多个字符
     *                        "*黄"或"?黄" 会引发全表（全索引）扫描 注意效率问题
     * @param tClass 返回实体
     * @param field  字段
     * @param param 查询参数
     * @param <T>
     * @return
     * @throws Exception
     */
    public <T> List<T> wildcardQuery(Class<T> tClass,String field,String param) throws Exception{
        QueryBuilder queryBuilder = QueryBuilders.wildcardQuery(field,param);
        return this.esbuildSelect(queryBuilder,tClass);
    }

    /**
     *  regexpQuery正则查询  \W：匹配包括下划线的任何单词字符，等价于 [A-Z a-z 0-9_]   开头的反斜杠是转义符
     *                      +号多次出现
     *                      (.)*为任意字符
     *                      正则查询取决于正则表达式的效率
     * @param tClass 返回实体
     * @param field  字段
     * @param param 查询参数
     * @param <T>
     * @return
     * @throws Exception
     */
    public <T> List<T> regexpQuery(Class<T> tClass,String field,String param) throws Exception{
        QueryBuilder queryBuilder = QueryBuilders.regexpQuery(field,param);
        return this.esbuildSelect(queryBuilder,tClass);
    }

    /**
     * 前缀查询  对keyword类型支持比较好
     * @param tClass 返回实体
     * @param field  字段
     * @param param 查询参数
     * @param <T>
     * @return
     * @throws Exception
     */
    public <T> List<T> prefixQuery(Class<T> tClass,String field,String param) throws Exception{
        QueryBuilder queryBuilder = QueryBuilders.prefixQuery(field, param);
        return this.esbuildSelect(queryBuilder,tClass);
    }

    /**
     * 范围查询      gt 大于，查询范围的最小值，也就是下界，但是不包含临界值。
     *              gte 大于等于，和 gt 的区别在于包含临界值。
     *              lt 小于，查询范围的最大值，也就是上界，但是不包含临界值。
     *              lte 小于等于，和 lt 的区别在于包含临界值
     * @param tClass 返回实体
     * @param field  字段
     * @param begin  开始
     * @param end    结束
     * @param <T>
     * @return
     * @throws Exception
     */
    public <T> List<T> rangeQuery(Class<T> tClass,String field,Object begin,Object end) throws Exception{
        RangeQueryBuilder rangeQuery = QueryBuilders.rangeQuery(field);
        rangeQuery.gte(begin);
        rangeQuery.lte(end);
        return this.esbuildSelect(rangeQuery,tClass);
    }

    /**
     *  bool查询，因为这个查询条件都是自己定制化的所以不封装，以下做个查询条件的例子。要使用直接使用esbuildSelect方法。
     *                                        查询品牌名称为:华为
     *                                        查询标题包含：手机
     *                                        查询价格在：2000-3000
     *
     *  boolQuery：对多个查询条件连接。连接方式：
     *    1、must（and）：条件必须成立
     *    2、must_not（not）：条件必须不成
     *    3、should（or）：条件可以成立
     *    4、filter：条件必须成立，性能比must高。不会计算得分
     *    **得分:**即条件匹配度,匹配度越高，得分越高

     * @param tClass
     * @param <T>
     * @return
     * @throws Exception
     */
    private  <T> List<T> boolQuery (Class<T> tClass) throws Exception{
        //1.构建boolQuery
        BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
        //2.构建各个查询条件
        //2.1 查询品牌名称为:华为
        TermQueryBuilder termQueryBuilder = QueryBuilders.termQuery("brandName", "华为");
        boolQuery.must(termQueryBuilder);
        //2.2. 查询标题包含：手机
        MatchQueryBuilder matchQuery = QueryBuilders.matchQuery("title", "手机");
        boolQuery.filter(matchQuery);

        //2.3 查询价格在：2000-3000
        RangeQueryBuilder rangeQuery = QueryBuilders.rangeQuery("price");
        rangeQuery.gte(2000);
        rangeQuery.lte(3000);
        boolQuery.filter(rangeQuery);

        return this.esbuildSelect(boolQuery,tClass);

    }

    /**
     *
     *  聚合查询：桶聚合，分组查询 该方法只作为一个示例
     *   1. 查询title包含手机的数据
     *   2. 查询品牌列表
     *
     *  •指标聚合：相当于MySQL的聚合函数。max、min、avg、sum等
     *  •桶聚合：相当于MySQL的 group by 操作。不要对text类型的数据进行分组，会失败。
     * @throws Exception
     */
    private void testAggQuery() throws Exception {
        SearchRequest searchRequest=new SearchRequest("goods");
        SearchSourceBuilder sourceBuilder=new SearchSourceBuilder();
        //1. 查询title包含手机的数据
        MatchQueryBuilder queryBuilder = QueryBuilders.matchQuery("title", "手机");
        sourceBuilder.query(queryBuilder);
        //2. 查询品牌列表  只展示前100条
        AggregationBuilder aggregation = AggregationBuilders.terms("goods_brands").field("brandName").size(100);
        sourceBuilder.aggregation(aggregation);


        searchRequest.source(sourceBuilder);

        SearchResponse searchResponse = esClient.search(searchRequest, RequestOptions.DEFAULT);

        //7. 获取命中对象 SearchHits
        SearchHits hits = searchResponse.getHits();

        //7.1 获取总记录数
        Long total= hits.getTotalHits().value;
        System.out.println("总数："+total);

        // aggregations 对象
        Aggregations aggregations = searchResponse.getAggregations();
        //将aggregations 转化为map
        Map<String, Aggregation> aggregationMap = aggregations.asMap();
        //通过key获取goods_brands 对象 使用Aggregation的子类接收  buckets属性在Terms接口中体现
        //Aggregation goods_brands1 = aggregationMap.get("goods_brands");
        Terms goods_brands =(Terms) aggregationMap.get("goods_brands");

        //获取buckets 数组集合
        List<? extends Terms.Bucket> buckets = goods_brands.getBuckets();

        Map<String,Object>map=new HashMap<>();
        //遍历buckets   key 属性名，doc_count 统计聚合数
        for (Terms.Bucket bucket : buckets) {

            System.out.println(bucket.getKey());
            map.put(bucket.getKeyAsString(),bucket.getDocCount());
        }

        System.out.println(map);

    }












}
